/*
 * The MIT License
 *
 * Copyright (c) 2016 Steven G. Brown
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package hudson.plugins.timestamper.action;

import hudson.plugins.timestamper.Timestamp;
import hudson.plugins.timestamper.format.ElapsedTimestampFormat;
import hudson.plugins.timestamper.format.SystemTimestampFormat;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import org.apache.commons.lang3.LocaleUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;

/**
 * Query for retrieving a page of time-stamps from {@link TimestampsActionOutput}.
 *
 * @author Steven G. Brown
 */
public final class TimestampsActionQuery {

    /**
     * Create a new {@link TimestampsActionQuery}.
     *
     * @param query the query string
     * @return a new query
     */
    public static TimestampsActionQuery create(String query) {
        int startLine = 0;
        Optional<Integer> endLine = Optional.empty();
        List<Function<Timestamp, String>> timestampFormats = new ArrayList<>();
        boolean appendLogLine = false;
        boolean currentTime = false;

        List<QueryParameter> queryParameters = readQueryString(query);

        Optional<String> timeZoneId = Optional.empty();
        Locale locale = Locale.getDefault();
        for (QueryParameter parameter : queryParameters) {
            if (parameter.name.equalsIgnoreCase("timeZone")) {
                // '+' was replaced with ' ' by URL decoding, so put it back.
                timeZoneId = Optional.of(parameter.value.replace("GMT ", "GMT+"));
            } else if (parameter.name.equalsIgnoreCase("locale")) {
                locale = LocaleUtils.toLocale(parameter.value);
            }
        }

        for (QueryParameter parameter : queryParameters) {
            if (parameter.name.equalsIgnoreCase("time")) {
                timestampFormats.add(new SystemTimestampFormat(parameter.value, timeZoneId, locale));
            } else if (parameter.name.equalsIgnoreCase("elapsed")) {
                timestampFormats.add(new ElapsedTimestampFormat(parameter.value));
            } else if (parameter.name.equalsIgnoreCase("precision")) {
                int precision = readPrecision(parameter.value);
                timestampFormats.add(new PrecisionTimestampFormat(precision));
            } else if (parameter.name.equalsIgnoreCase("appendLog")) {
                appendLogLine = parameter.value.isEmpty() || Boolean.parseBoolean(parameter.value);
            } else if (parameter.name.equalsIgnoreCase("currentTime")) {
                currentTime = parameter.value.isEmpty() || Boolean.parseBoolean(parameter.value);
            } else if (parameter.name.equalsIgnoreCase("startLine")) {
                startLine = Integer.parseInt(parameter.value);
            } else if (parameter.name.equalsIgnoreCase("endLine")) {
                endLine = Optional.of(Integer.valueOf(parameter.value));
            }
        }

        if (timestampFormats.isEmpty()) {
            // Default
            timestampFormats.add(new PrecisionTimestampFormat(3));
        }

        return new TimestampsActionQuery(startLine, endLine, timestampFormats, appendLogLine, currentTime);
    }

    private static List<QueryParameter> readQueryString(String query) {
        List<QueryParameter> parameters = new ArrayList<>();
        if (query != null) {
            String[] pairs = query.split("&");
            for (String pair : pairs) {
                String[] nameAndValue = pair.split("=", 2);
                String name = urlDecode(nameAndValue[0]);
                String value = nameAndValue.length == 1 ? "" : urlDecode(nameAndValue[1]);
                parameters.add(new QueryParameter(name, value));
            }
        }
        return Collections.unmodifiableList(parameters);
    }

    private static String urlDecode(String string) {
        try {
            return URLDecoder.decode(string, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private static int readPrecision(String precision) {
        if ("seconds".equalsIgnoreCase(precision)) {
            return 0;
        }
        if ("milliseconds".equalsIgnoreCase(precision)) {
            return 3;
        }
        if ("microseconds".equalsIgnoreCase(precision)) {
            return 6;
        }
        if ("nanoseconds".equalsIgnoreCase(precision)) {
            return 9;
        }
        int intPrecision = Integer.parseInt(precision);
        if (intPrecision < 0) {
            throw new IllegalArgumentException("Expected non-negative precision, but was: " + precision);
        }
        return intPrecision;
    }

    private static class QueryParameter {

        final String name;

        final String value;

        QueryParameter(String name, String value) {
            this.name = Objects.requireNonNull(name);
            this.value = Objects.requireNonNull(value);
        }

        @Override
        public String toString() {
            return name + "=" + value;
        }
    }

    final int startLine;

    final Optional<Integer> endLine;

    final List<Function<Timestamp, String>> timestampFormats;

    final boolean appendLogLine;

    final boolean currentTime;

    TimestampsActionQuery(
            int startLine,
            Optional<Integer> endLine,
            List<? extends Function<Timestamp, String>> timestampFormats,
            boolean appendLogLine,
            boolean currentTime) {
        this.startLine = startLine;
        this.endLine = Objects.requireNonNull(endLine);
        this.timestampFormats = Collections.unmodifiableList(new ArrayList<>(timestampFormats));
        this.appendLogLine = appendLogLine;
        this.currentTime = currentTime;
    }

    /** {@inheritDoc} */
    @Override
    public int hashCode() {
        return Objects.hash(startLine, endLine, timestampFormats, appendLogLine, currentTime);
    }

    /** {@inheritDoc} */
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof TimestampsActionQuery) {
            TimestampsActionQuery other = (TimestampsActionQuery) obj;
            return startLine == other.startLine
                    && endLine.equals(other.endLine)
                    && timestampFormats.equals(other.timestampFormats)
                    && appendLogLine == other.appendLogLine
                    && currentTime == other.currentTime;
        }
        return false;
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return new ToStringBuilder(this)
                .append("startLine", startLine)
                .append("endLine", endLine)
                .append("timestampFormats", timestampFormats)
                .append("appendLogLine", appendLogLine)
                .append("currentTime", currentTime)
                .toString();
    }
}
